{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2024-07-19T07:18:47.562287Z",
     "start_time": "2024-07-19T07:18:47.397909900Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "sys.version_info(major=3, minor=12, micro=3, releaselevel='final', serial=0)\n",
      "matplotlib 3.9.0\n",
      "numpy 1.26.4\n",
      "pandas 2.2.2\n",
      "sklearn 1.5.0\n",
      "torch 2.3.1+cpu\n",
      "cpu\n"
     ]
    }
   ],
   "source": [
    "import matplotlib as mpl\n",
    "import matplotlib.pyplot as plt\n",
    "%matplotlib inline\n",
    "import numpy as np\n",
    "import sklearn\n",
    "import pandas as pd\n",
    "import os\n",
    "import sys\n",
    "import time\n",
    "from tqdm.auto import tqdm\n",
    "import torch\n",
    "import torch.nn as nn\n",
    "import torch.nn.functional as F\n",
    "\n",
    "print(sys.version_info)\n",
    "for module in mpl, np, pd, sklearn, torch:\n",
    "    print(module.__name__, module.__version__)\n",
    "    \n",
    "device = torch.device(\"cuda:0\") if torch.cuda.is_available() else torch.device(\"cpu\")\n",
    "print(device)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 准备数据"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2024-07-19T07:18:47.681278900Z",
     "start_time": "2024-07-19T07:18:47.415987900Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      ".. _california_housing_dataset:\n",
      "\n",
      "California Housing dataset\n",
      "--------------------------\n",
      "\n",
      "**Data Set Characteristics:**\n",
      "\n",
      ":Number of Instances: 20640\n",
      "\n",
      ":Number of Attributes: 8 numeric, predictive attributes and the target\n",
      "\n",
      ":Attribute Information:\n",
      "    - MedInc        median income in block group\n",
      "    - HouseAge      median house age in block group\n",
      "    - AveRooms      average number of rooms per household\n",
      "    - AveBedrms     average number of bedrooms per household\n",
      "    - Population    block group population\n",
      "    - AveOccup      average number of household members\n",
      "    - Latitude      block group latitude\n",
      "    - Longitude     block group longitude\n",
      "\n",
      ":Missing Attribute Values: None\n",
      "\n",
      "This dataset was obtained from the StatLib repository.\n",
      "https://www.dcc.fc.up.pt/~ltorgo/Regression/cal_housing.html\n",
      "\n",
      "The target variable is the median house value for California districts,\n",
      "expressed in hundreds of thousands of dollars ($100,000).\n",
      "\n",
      "This dataset was derived from the 1990 U.S. census, using one row per census\n",
      "block group. A block group is the smallest geographical unit for which the U.S.\n",
      "Census Bureau publishes sample data (a block group typically has a population\n",
      "of 600 to 3,000 people).\n",
      "\n",
      "A household is a group of people residing within a home. Since the average\n",
      "number of rooms and bedrooms in this dataset are provided per household, these\n",
      "columns may take surprisingly large values for block groups with few households\n",
      "and many empty houses, such as vacation resorts.\n",
      "\n",
      "It can be downloaded/loaded using the\n",
      ":func:`sklearn.datasets.fetch_california_housing` function.\n",
      "\n",
      ".. topic:: References\n",
      "\n",
      "    - Pace, R. Kelley and Ronald Barry, Sparse Spatial Autoregressions,\n",
      "      Statistics and Probability Letters, 33 (1997) 291-297\n",
      "\n",
      "(20640, 8)\n",
      "(20640,)\n"
     ]
    }
   ],
   "source": [
    "from sklearn.datasets import fetch_california_housing\n",
    "\n",
    "housing = fetch_california_housing()\n",
    "print(housing.DESCR)\n",
    "print(housing.data.shape)\n",
    "print(housing.target.shape)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2024-07-19T07:18:47.682279100Z",
     "start_time": "2024-07-19T07:18:47.444449800Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "array([[ 8.32520000e+00,  4.10000000e+01,  6.98412698e+00,\n",
      "         1.02380952e+00,  3.22000000e+02,  2.55555556e+00,\n",
      "         3.78800000e+01, -1.22230000e+02],\n",
      "       [ 8.30140000e+00,  2.10000000e+01,  6.23813708e+00,\n",
      "         9.71880492e-01,  2.40100000e+03,  2.10984183e+00,\n",
      "         3.78600000e+01, -1.22220000e+02],\n",
      "       [ 7.25740000e+00,  5.20000000e+01,  8.28813559e+00,\n",
      "         1.07344633e+00,  4.96000000e+02,  2.80225989e+00,\n",
      "         3.78500000e+01, -1.22240000e+02],\n",
      "       [ 5.64310000e+00,  5.20000000e+01,  5.81735160e+00,\n",
      "         1.07305936e+00,  5.58000000e+02,  2.54794521e+00,\n",
      "         3.78500000e+01, -1.22250000e+02],\n",
      "       [ 3.84620000e+00,  5.20000000e+01,  6.28185328e+00,\n",
      "         1.08108108e+00,  5.65000000e+02,  2.18146718e+00,\n",
      "         3.78500000e+01, -1.22250000e+02]])\n",
      "--------------------------------------------------\n",
      "array([4.526, 3.585, 3.521, 3.413, 3.422])\n"
     ]
    }
   ],
   "source": [
    "# print(housing.data[0:5])\n",
    "import pprint  #打印的格式比较 好看\n",
    "\n",
    "pprint.pprint(housing.data[0:5])\n",
    "print('-'*50)\n",
    "pprint.pprint(housing.target[0:5])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2024-07-19T07:18:47.684795100Z",
     "start_time": "2024-07-19T07:18:47.453273400Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "(11610, 8) (11610,)\n",
      "(3870, 8) (3870,)\n",
      "(5160, 8) (5160,)\n"
     ]
    }
   ],
   "source": [
    "from sklearn.model_selection import train_test_split\n",
    "\n",
    "#拆分训练集和测试集，random_state是随机种子,同样的随机数种子，是为了得到同样的随机值\n",
    "x_train_all, x_test, y_train_all, y_test = train_test_split(\n",
    "    housing.data, housing.target, random_state = 7)\n",
    "x_train, x_valid, y_train, y_valid = train_test_split(\n",
    "    x_train_all, y_train_all, random_state = 11)\n",
    "# 训练集\n",
    "print(x_train.shape, y_train.shape)\n",
    "# 验证集\n",
    "print(x_valid.shape, y_valid.shape)\n",
    "# 测试集\n",
    "print(x_test.shape, y_test.shape)\n",
    "\n",
    "dataset_maps = {\n",
    "    \"train\": [x_train, y_train],\n",
    "    \"valid\": [x_valid, y_valid],\n",
    "    \"test\": [x_test, y_test],\n",
    "}\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2024-07-19T07:18:47.698778900Z",
     "start_time": "2024-07-19T07:18:47.468940100Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": "StandardScaler()",
      "text/html": "<style>#sk-container-id-2 {\n  /* Definition of color scheme common for light and dark mode */\n  --sklearn-color-text: black;\n  --sklearn-color-line: gray;\n  /* Definition of color scheme for unfitted estimators */\n  --sklearn-color-unfitted-level-0: #fff5e6;\n  --sklearn-color-unfitted-level-1: #f6e4d2;\n  --sklearn-color-unfitted-level-2: #ffe0b3;\n  --sklearn-color-unfitted-level-3: chocolate;\n  /* Definition of color scheme for fitted estimators */\n  --sklearn-color-fitted-level-0: #f0f8ff;\n  --sklearn-color-fitted-level-1: #d4ebff;\n  --sklearn-color-fitted-level-2: #b3dbfd;\n  --sklearn-color-fitted-level-3: cornflowerblue;\n\n  /* Specific color for light theme */\n  --sklearn-color-text-on-default-background: var(--sg-text-color, var(--theme-code-foreground, var(--jp-content-font-color1, black)));\n  --sklearn-color-background: var(--sg-background-color, var(--theme-background, var(--jp-layout-color0, white)));\n  --sklearn-color-border-box: var(--sg-text-color, var(--theme-code-foreground, var(--jp-content-font-color1, black)));\n  --sklearn-color-icon: #696969;\n\n  @media (prefers-color-scheme: dark) {\n    /* Redefinition of color scheme for dark theme */\n    --sklearn-color-text-on-default-background: var(--sg-text-color, var(--theme-code-foreground, var(--jp-content-font-color1, white)));\n    --sklearn-color-background: var(--sg-background-color, var(--theme-background, var(--jp-layout-color0, #111)));\n    --sklearn-color-border-box: var(--sg-text-color, var(--theme-code-foreground, var(--jp-content-font-color1, white)));\n    --sklearn-color-icon: #878787;\n  }\n}\n\n#sk-container-id-2 {\n  color: var(--sklearn-color-text);\n}\n\n#sk-container-id-2 pre {\n  padding: 0;\n}\n\n#sk-container-id-2 input.sk-hidden--visually {\n  border: 0;\n  clip: rect(1px 1px 1px 1px);\n  clip: rect(1px, 1px, 1px, 1px);\n  height: 1px;\n  margin: -1px;\n  overflow: hidden;\n  padding: 0;\n  position: absolute;\n  width: 1px;\n}\n\n#sk-container-id-2 div.sk-dashed-wrapped {\n  border: 1px dashed var(--sklearn-color-line);\n  margin: 0 0.4em 0.5em 0.4em;\n  box-sizing: border-box;\n  padding-bottom: 0.4em;\n  background-color: var(--sklearn-color-background);\n}\n\n#sk-container-id-2 div.sk-container {\n  /* jupyter's `normalize.less` sets `[hidden] { display: none; }`\n     but bootstrap.min.css set `[hidden] { display: none !important; }`\n     so we also need the `!important` here to be able to override the\n     default hidden behavior on the sphinx rendered scikit-learn.org.\n     See: https://github.com/scikit-learn/scikit-learn/issues/21755 */\n  display: inline-block !important;\n  position: relative;\n}\n\n#sk-container-id-2 div.sk-text-repr-fallback {\n  display: none;\n}\n\ndiv.sk-parallel-item,\ndiv.sk-serial,\ndiv.sk-item {\n  /* draw centered vertical line to link estimators */\n  background-image: linear-gradient(var(--sklearn-color-text-on-default-background), var(--sklearn-color-text-on-default-background));\n  background-size: 2px 100%;\n  background-repeat: no-repeat;\n  background-position: center center;\n}\n\n/* Parallel-specific style estimator block */\n\n#sk-container-id-2 div.sk-parallel-item::after {\n  content: \"\";\n  width: 100%;\n  border-bottom: 2px solid var(--sklearn-color-text-on-default-background);\n  flex-grow: 1;\n}\n\n#sk-container-id-2 div.sk-parallel {\n  display: flex;\n  align-items: stretch;\n  justify-content: center;\n  background-color: var(--sklearn-color-background);\n  position: relative;\n}\n\n#sk-container-id-2 div.sk-parallel-item {\n  display: flex;\n  flex-direction: column;\n}\n\n#sk-container-id-2 div.sk-parallel-item:first-child::after {\n  align-self: flex-end;\n  width: 50%;\n}\n\n#sk-container-id-2 div.sk-parallel-item:last-child::after {\n  align-self: flex-start;\n  width: 50%;\n}\n\n#sk-container-id-2 div.sk-parallel-item:only-child::after {\n  width: 0;\n}\n\n/* Serial-specific style estimator block */\n\n#sk-container-id-2 div.sk-serial {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  background-color: var(--sklearn-color-background);\n  padding-right: 1em;\n  padding-left: 1em;\n}\n\n\n/* Toggleable style: style used for estimator/Pipeline/ColumnTransformer box that is\nclickable and can be expanded/collapsed.\n- Pipeline and ColumnTransformer use this feature and define the default style\n- Estimators will overwrite some part of the style using the `sk-estimator` class\n*/\n\n/* Pipeline and ColumnTransformer style (default) */\n\n#sk-container-id-2 div.sk-toggleable {\n  /* Default theme specific background. It is overwritten whether we have a\n  specific estimator or a Pipeline/ColumnTransformer */\n  background-color: var(--sklearn-color-background);\n}\n\n/* Toggleable label */\n#sk-container-id-2 label.sk-toggleable__label {\n  cursor: pointer;\n  display: block;\n  width: 100%;\n  margin-bottom: 0;\n  padding: 0.5em;\n  box-sizing: border-box;\n  text-align: center;\n}\n\n#sk-container-id-2 label.sk-toggleable__label-arrow:before {\n  /* Arrow on the left of the label */\n  content: \"▸\";\n  float: left;\n  margin-right: 0.25em;\n  color: var(--sklearn-color-icon);\n}\n\n#sk-container-id-2 label.sk-toggleable__label-arrow:hover:before {\n  color: var(--sklearn-color-text);\n}\n\n/* Toggleable content - dropdown */\n\n#sk-container-id-2 div.sk-toggleable__content {\n  max-height: 0;\n  max-width: 0;\n  overflow: hidden;\n  text-align: left;\n  /* unfitted */\n  background-color: var(--sklearn-color-unfitted-level-0);\n}\n\n#sk-container-id-2 div.sk-toggleable__content.fitted {\n  /* fitted */\n  background-color: var(--sklearn-color-fitted-level-0);\n}\n\n#sk-container-id-2 div.sk-toggleable__content pre {\n  margin: 0.2em;\n  border-radius: 0.25em;\n  color: var(--sklearn-color-text);\n  /* unfitted */\n  background-color: var(--sklearn-color-unfitted-level-0);\n}\n\n#sk-container-id-2 div.sk-toggleable__content.fitted pre {\n  /* unfitted */\n  background-color: var(--sklearn-color-fitted-level-0);\n}\n\n#sk-container-id-2 input.sk-toggleable__control:checked~div.sk-toggleable__content {\n  /* Expand drop-down */\n  max-height: 200px;\n  max-width: 100%;\n  overflow: auto;\n}\n\n#sk-container-id-2 input.sk-toggleable__control:checked~label.sk-toggleable__label-arrow:before {\n  content: \"▾\";\n}\n\n/* Pipeline/ColumnTransformer-specific style */\n\n#sk-container-id-2 div.sk-label input.sk-toggleable__control:checked~label.sk-toggleable__label {\n  color: var(--sklearn-color-text);\n  background-color: var(--sklearn-color-unfitted-level-2);\n}\n\n#sk-container-id-2 div.sk-label.fitted input.sk-toggleable__control:checked~label.sk-toggleable__label {\n  background-color: var(--sklearn-color-fitted-level-2);\n}\n\n/* Estimator-specific style */\n\n/* Colorize estimator box */\n#sk-container-id-2 div.sk-estimator input.sk-toggleable__control:checked~label.sk-toggleable__label {\n  /* unfitted */\n  background-color: var(--sklearn-color-unfitted-level-2);\n}\n\n#sk-container-id-2 div.sk-estimator.fitted input.sk-toggleable__control:checked~label.sk-toggleable__label {\n  /* fitted */\n  background-color: var(--sklearn-color-fitted-level-2);\n}\n\n#sk-container-id-2 div.sk-label label.sk-toggleable__label,\n#sk-container-id-2 div.sk-label label {\n  /* The background is the default theme color */\n  color: var(--sklearn-color-text-on-default-background);\n}\n\n/* On hover, darken the color of the background */\n#sk-container-id-2 div.sk-label:hover label.sk-toggleable__label {\n  color: var(--sklearn-color-text);\n  background-color: var(--sklearn-color-unfitted-level-2);\n}\n\n/* Label box, darken color on hover, fitted */\n#sk-container-id-2 div.sk-label.fitted:hover label.sk-toggleable__label.fitted {\n  color: var(--sklearn-color-text);\n  background-color: var(--sklearn-color-fitted-level-2);\n}\n\n/* Estimator label */\n\n#sk-container-id-2 div.sk-label label {\n  font-family: monospace;\n  font-weight: bold;\n  display: inline-block;\n  line-height: 1.2em;\n}\n\n#sk-container-id-2 div.sk-label-container {\n  text-align: center;\n}\n\n/* Estimator-specific */\n#sk-container-id-2 div.sk-estimator {\n  font-family: monospace;\n  border: 1px dotted var(--sklearn-color-border-box);\n  border-radius: 0.25em;\n  box-sizing: border-box;\n  margin-bottom: 0.5em;\n  /* unfitted */\n  background-color: var(--sklearn-color-unfitted-level-0);\n}\n\n#sk-container-id-2 div.sk-estimator.fitted {\n  /* fitted */\n  background-color: var(--sklearn-color-fitted-level-0);\n}\n\n/* on hover */\n#sk-container-id-2 div.sk-estimator:hover {\n  /* unfitted */\n  background-color: var(--sklearn-color-unfitted-level-2);\n}\n\n#sk-container-id-2 div.sk-estimator.fitted:hover {\n  /* fitted */\n  background-color: var(--sklearn-color-fitted-level-2);\n}\n\n/* Specification for estimator info (e.g. \"i\" and \"?\") */\n\n/* Common style for \"i\" and \"?\" */\n\n.sk-estimator-doc-link,\na:link.sk-estimator-doc-link,\na:visited.sk-estimator-doc-link {\n  float: right;\n  font-size: smaller;\n  line-height: 1em;\n  font-family: monospace;\n  background-color: var(--sklearn-color-background);\n  border-radius: 1em;\n  height: 1em;\n  width: 1em;\n  text-decoration: none !important;\n  margin-left: 1ex;\n  /* unfitted */\n  border: var(--sklearn-color-unfitted-level-1) 1pt solid;\n  color: var(--sklearn-color-unfitted-level-1);\n}\n\n.sk-estimator-doc-link.fitted,\na:link.sk-estimator-doc-link.fitted,\na:visited.sk-estimator-doc-link.fitted {\n  /* fitted */\n  border: var(--sklearn-color-fitted-level-1) 1pt solid;\n  color: var(--sklearn-color-fitted-level-1);\n}\n\n/* On hover */\ndiv.sk-estimator:hover .sk-estimator-doc-link:hover,\n.sk-estimator-doc-link:hover,\ndiv.sk-label-container:hover .sk-estimator-doc-link:hover,\n.sk-estimator-doc-link:hover {\n  /* unfitted */\n  background-color: var(--sklearn-color-unfitted-level-3);\n  color: var(--sklearn-color-background);\n  text-decoration: none;\n}\n\ndiv.sk-estimator.fitted:hover .sk-estimator-doc-link.fitted:hover,\n.sk-estimator-doc-link.fitted:hover,\ndiv.sk-label-container:hover .sk-estimator-doc-link.fitted:hover,\n.sk-estimator-doc-link.fitted:hover {\n  /* fitted */\n  background-color: var(--sklearn-color-fitted-level-3);\n  color: var(--sklearn-color-background);\n  text-decoration: none;\n}\n\n/* Span, style for the box shown on hovering the info icon */\n.sk-estimator-doc-link span {\n  display: none;\n  z-index: 9999;\n  position: relative;\n  font-weight: normal;\n  right: .2ex;\n  padding: .5ex;\n  margin: .5ex;\n  width: min-content;\n  min-width: 20ex;\n  max-width: 50ex;\n  color: var(--sklearn-color-text);\n  box-shadow: 2pt 2pt 4pt #999;\n  /* unfitted */\n  background: var(--sklearn-color-unfitted-level-0);\n  border: .5pt solid var(--sklearn-color-unfitted-level-3);\n}\n\n.sk-estimator-doc-link.fitted span {\n  /* fitted */\n  background: var(--sklearn-color-fitted-level-0);\n  border: var(--sklearn-color-fitted-level-3);\n}\n\n.sk-estimator-doc-link:hover span {\n  display: block;\n}\n\n/* \"?\"-specific style due to the `<a>` HTML tag */\n\n#sk-container-id-2 a.estimator_doc_link {\n  float: right;\n  font-size: 1rem;\n  line-height: 1em;\n  font-family: monospace;\n  background-color: var(--sklearn-color-background);\n  border-radius: 1rem;\n  height: 1rem;\n  width: 1rem;\n  text-decoration: none;\n  /* unfitted */\n  color: var(--sklearn-color-unfitted-level-1);\n  border: var(--sklearn-color-unfitted-level-1) 1pt solid;\n}\n\n#sk-container-id-2 a.estimator_doc_link.fitted {\n  /* fitted */\n  border: var(--sklearn-color-fitted-level-1) 1pt solid;\n  color: var(--sklearn-color-fitted-level-1);\n}\n\n/* On hover */\n#sk-container-id-2 a.estimator_doc_link:hover {\n  /* unfitted */\n  background-color: var(--sklearn-color-unfitted-level-3);\n  color: var(--sklearn-color-background);\n  text-decoration: none;\n}\n\n#sk-container-id-2 a.estimator_doc_link.fitted:hover {\n  /* fitted */\n  background-color: var(--sklearn-color-fitted-level-3);\n}\n</style><div id=\"sk-container-id-2\" class=\"sk-top-container\"><div class=\"sk-text-repr-fallback\"><pre>StandardScaler()</pre><b>In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook. <br />On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.</b></div><div class=\"sk-container\" hidden><div class=\"sk-item\"><div class=\"sk-estimator fitted sk-toggleable\"><input class=\"sk-toggleable__control sk-hidden--visually\" id=\"sk-estimator-id-2\" type=\"checkbox\" checked><label for=\"sk-estimator-id-2\" class=\"sk-toggleable__label fitted sk-toggleable__label-arrow fitted\">&nbsp;&nbsp;StandardScaler<a class=\"sk-estimator-doc-link fitted\" rel=\"noreferrer\" target=\"_blank\" href=\"https://scikit-learn.org/1.5/modules/generated/sklearn.preprocessing.StandardScaler.html\">?<span>Documentation for StandardScaler</span></a><span class=\"sk-estimator-doc-link fitted\">i<span>Fitted</span></span></label><div class=\"sk-toggleable__content fitted\"><pre>StandardScaler()</pre></div> </div></div></div></div>"
     },
     "execution_count": 15,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from sklearn.preprocessing import StandardScaler\n",
    "from torch.utils.data import DataLoader\n",
    "\n",
    "\n",
    "scaler = StandardScaler()\n",
    "scaler.fit(x_train)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 构建数据集"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2024-07-19T07:18:47.698778900Z",
     "start_time": "2024-07-19T07:18:47.483524800Z"
    }
   },
   "outputs": [],
   "source": [
    "from torch.utils.data import Dataset\n",
    "\n",
    "class HousingDataset(Dataset):\n",
    "    def __init__(self, mode='train'):\n",
    "        self.x, self.y = dataset_maps[mode]\n",
    "        self.x = torch.from_numpy(scaler.transform(self.x)).float()\n",
    "        self.y = torch.from_numpy(self.y).float().reshape(-1, 1)\n",
    "            \n",
    "    def __len__(self):\n",
    "        return len(self.x)\n",
    "    \n",
    "    def __getitem__(self, idx):\n",
    "        return self.x[idx], self.y[idx]\n",
    "    \n",
    "    \n",
    "train_ds = HousingDataset(\"train\")\n",
    "valid_ds = HousingDataset(\"valid\")\n",
    "test_ds = HousingDataset(\"test\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2024-07-19T07:18:47.699777900Z",
     "start_time": "2024-07-19T07:18:47.497956400Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": "(tensor([-0.2981,  0.3523, -0.1092, -0.2506, -0.0341, -0.0060,  1.0806, -1.0611]),\n tensor([1.5140]))"
     },
     "execution_count": 17,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "train_ds[1]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### DataLoader"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2024-07-19T07:18:47.699777900Z",
     "start_time": "2024-07-19T07:18:47.508020600Z"
    }
   },
   "outputs": [],
   "source": [
    "from torch.utils.data import DataLoader\n",
    "\n",
    "\n",
    "batch_size = 16\n",
    "train_loader = DataLoader(train_ds, batch_size=batch_size, shuffle=True)\n",
    "val_loader = DataLoader(valid_ds, batch_size=batch_size, shuffle=False)\n",
    "test_loader = DataLoader(test_ds, batch_size=batch_size, shuffle=False)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 定义模型"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "自定义 Layer"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2024-07-19T07:18:47.715769Z",
     "start_time": "2024-07-19T07:18:47.521525900Z"
    }
   },
   "outputs": [],
   "source": [
    "class CustomizedLinear(nn.Module):\n",
    "    def __init__(self, in_features, out_features):\n",
    "        super().__init__()\n",
    "        # 感兴趣的可以在这里实现其他的初始化方法\n",
    "        self.weights = nn.Parameter(torch.zeros(in_features, out_features)) # 定义权重\n",
    "        self.weights = nn.init.xavier_normal_(self.weights) # 使用xavier_normal初始化\n",
    "        self.bias = nn.Parameter(torch.zeros(out_features))  #Parameter是nn.Module的子类，可以被自动求导，可以被优化器优化\n",
    "        \n",
    "    def forward(self, x):\n",
    "        return torch.mm(x, self.weights) + self.bias # 全连接层的前向计算,mm是矩阵乘法"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2024-07-19T07:18:47.715769Z",
     "start_time": "2024-07-19T07:18:47.534790600Z"
    }
   },
   "outputs": [],
   "source": [
    "#回归模型我们只需要1个数\n",
    "\n",
    "class NeuralNetwork(nn.Module):\n",
    "    def __init__(self, input_dim=8):\n",
    "        super().__init__()\n",
    "        self.linear_relu_stack = nn.Sequential(\n",
    "            CustomizedLinear(input_dim, 30),\n",
    "            nn.ReLU(),\n",
    "            CustomizedLinear(30, 1)\n",
    "            )\n",
    "        \n",
    "    def forward(self, x):\n",
    "        # x.shape [batch size, 8]\n",
    "        logits = self.linear_relu_stack(x)\n",
    "        # logits.shape [batch size, 1]\n",
    "        return logits"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "linear_relu_stack.0.weights torch.Size([8, 30])\n",
      "linear_relu_stack.0.bias torch.Size([30])\n",
      "linear_relu_stack.2.weights torch.Size([30, 1])\n",
      "linear_relu_stack.2.bias torch.Size([1])\n"
     ]
    }
   ],
   "source": [
    "model = NeuralNetwork()\n",
    "for name, param in model.named_parameters():\n",
    "    print(name, param.shape)"
   ],
   "metadata": {
    "collapsed": false,
    "ExecuteTime": {
     "end_time": "2024-07-19T07:18:47.716768100Z",
     "start_time": "2024-07-19T07:18:47.550293100Z"
    }
   }
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "NeuralNetwork(\n",
      "  (linear_relu_stack): Sequential(\n",
      "    (0): CustomizedLinear()\n",
      "    (1): ReLU()\n",
      "    (2): CustomizedLinear()\n",
      "  )\n",
      ")\n",
      "--------------------------------------------------\n",
      "Sequential(\n",
      "  (0): CustomizedLinear()\n",
      "  (1): ReLU()\n",
      "  (2): CustomizedLinear()\n",
      ")\n",
      "--------------------------------------------------\n",
      "CustomizedLinear()\n",
      "--------------------------------------------------\n",
      "ReLU()\n",
      "--------------------------------------------------\n",
      "CustomizedLinear()\n",
      "--------------------------------------------------\n"
     ]
    }
   ],
   "source": [
    "for m in model.modules():\n",
    "    print(m)\n",
    "    print('-'*50)"
   ],
   "metadata": {
    "collapsed": false,
    "ExecuteTime": {
     "end_time": "2024-07-19T07:19:34.645890800Z",
     "start_time": "2024-07-19T07:19:34.617848300Z"
    }
   }
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2024-07-19T07:21:00.506145500Z",
     "start_time": "2024-07-19T07:21:00.438360Z"
    }
   },
   "outputs": [],
   "source": [
    "class EarlyStopCallback:\n",
    "    def __init__(self, patience=5, min_delta=0.01):\n",
    "        \"\"\"\n",
    "\n",
    "        Args:\n",
    "            patience (int, optional): Number of epochs with no improvement after which training will be stopped.. Defaults to 5.\n",
    "            min_delta (float, optional): Minimum change in the monitored quantity to qualify as an improvement, i.e. an absolute \n",
    "                change of less than min_delta, will count as no improvement. Defaults to 0.01.\n",
    "        \"\"\"\n",
    "        self.patience = patience\n",
    "        self.min_delta = min_delta\n",
    "        self.best_metric = -1\n",
    "        self.counter = 0\n",
    "        \n",
    "    def __call__(self, metric):\n",
    "        if metric >= self.best_metric + self.min_delta:\n",
    "            # update best metric\n",
    "            self.best_metric = metric\n",
    "            # reset counter \n",
    "            self.counter = 0\n",
    "        else: \n",
    "            self.counter += 1\n",
    "            \n",
    "    @property\n",
    "    def early_stop(self):\n",
    "        return self.counter >= self.patience\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2024-07-19T07:21:00.544127700Z",
     "start_time": "2024-07-19T07:21:00.447657300Z"
    }
   },
   "outputs": [],
   "source": [
    "from sklearn.metrics import accuracy_score\n",
    "\n",
    "@torch.no_grad()\n",
    "def evaluating(model, dataloader, loss_fct):\n",
    "    loss_list = []\n",
    "    for datas, labels in dataloader:\n",
    "        datas = datas.to(device)\n",
    "        labels = labels.to(device)\n",
    "        # 前向计算\n",
    "        logits = model(datas)\n",
    "        loss = loss_fct(logits, labels)         # 验证集损失\n",
    "        loss_list.append(loss.item())\n",
    "        \n",
    "    return np.mean(loss_list)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 27,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2024-07-19T07:22:25.518781900Z",
     "start_time": "2024-07-19T07:21:00.462171300Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": "  0%|          | 0/72600 [00:00<?, ?it/s]",
      "application/vnd.jupyter.widget-view+json": {
       "version_major": 2,
       "version_minor": 0,
       "model_id": "df49cae90d554e0ba27a99df7638edc4"
      }
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Early stop at epoch 50 / global_step 36300\n"
     ]
    }
   ],
   "source": [
    "# 训练\n",
    "def training(\n",
    "    model, \n",
    "    train_loader, \n",
    "    val_loader, \n",
    "    epoch, \n",
    "    loss_fct, \n",
    "    optimizer, \n",
    "    tensorboard_callback=None,\n",
    "    save_ckpt_callback=None,\n",
    "    early_stop_callback=None,\n",
    "    eval_step=500,\n",
    "    ):\n",
    "    record_dict = {\n",
    "        \"train\": [],\n",
    "        \"val\": []\n",
    "    }\n",
    "    \n",
    "    global_step = 0\n",
    "    model.train()\n",
    "    with tqdm(total=epoch * len(train_loader)) as pbar:\n",
    "        for epoch_id in range(epoch):\n",
    "            # training\n",
    "            for datas, labels in train_loader:\n",
    "                datas = datas.to(device)\n",
    "                labels = labels.to(device)\n",
    "                # 梯度清空\n",
    "                optimizer.zero_grad()\n",
    "                # 模型前向计算\n",
    "                logits = model(datas)\n",
    "                # 计算损失\n",
    "                loss = loss_fct(logits, labels)\n",
    "                # 梯度回传\n",
    "                loss.backward()\n",
    "                # 调整优化器，包括学习率的变动等\n",
    "                optimizer.step()\n",
    " \n",
    "                loss = loss.cpu().item()\n",
    "                # record\n",
    "                \n",
    "                record_dict[\"train\"].append({\n",
    "                    \"loss\": loss, \"step\": global_step\n",
    "                })\n",
    "                \n",
    "                # evaluating\n",
    "                if global_step % eval_step == 0:\n",
    "                    model.eval()\n",
    "                    val_loss = evaluating(model, val_loader, loss_fct)\n",
    "                    record_dict[\"val\"].append({\n",
    "                        \"loss\": val_loss, \"step\": global_step\n",
    "                    })\n",
    "                    model.train()\n",
    "\n",
    "                    # 早停 Early Stop\n",
    "                    if early_stop_callback is not None:\n",
    "                        early_stop_callback(-val_loss)\n",
    "                        if early_stop_callback.early_stop:\n",
    "                            print(f\"Early stop at epoch {epoch_id} / global_step {global_step}\")\n",
    "                            return record_dict\n",
    "                    \n",
    "                # udate step\n",
    "                global_step += 1\n",
    "                pbar.update(1)\n",
    "                pbar.set_postfix({\"epoch\": epoch_id})\n",
    "        \n",
    "    return record_dict\n",
    "        \n",
    "\n",
    "epoch = 100\n",
    "\n",
    "model = NeuralNetwork()\n",
    "\n",
    "# 1. 定义损失函数 采用MSE损失\n",
    "loss_fct = nn.MSELoss()\n",
    "# 2. 定义优化器 采用SGD\n",
    "# Optimizers specified in the torch.optim package\n",
    "optimizer = torch.optim.SGD(model.parameters(), lr=0.001, momentum=0.9)\n",
    "\n",
    "# 3. early stop\n",
    "early_stop_callback = EarlyStopCallback(patience=10, min_delta=1e-3)\n",
    "\n",
    "model = model.to(device)\n",
    "record = training(\n",
    "    model, \n",
    "    train_loader, \n",
    "    val_loader, \n",
    "    epoch, \n",
    "    loss_fct, \n",
    "    optimizer, \n",
    "    early_stop_callback=early_stop_callback,\n",
    "    eval_step=len(train_loader)\n",
    "    )"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 30,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2024-07-19T08:00:28.862984600Z",
     "start_time": "2024-07-19T08:00:28.700435600Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": "<Figure size 640x480 with 1 Axes>",
      "image/png": "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yqLUfgPK7XtT7PfFKh4OnHBKREREHsXwQURERB6lqPCh1+vx4osvQq/Xe7spHsV+s99KwH4rq9+Acvt+NfS7yU04JSIioquboiofRERE5H0MH0RERORRDB9ERETkUQwfRERE5FGKCR/vvvsuWrRoAR8fH/Tu3RtbtmzxdpMaZdasWZAkqdpH+/btnbcbDAZMmTIF4eHhCAgIwC233IK8vLxqx8jKysKoUaPg5+eHqKgoPPXUU7BYLNX2SUtLQ7du3aDX69G6dWt89tlnnuie05o1azB69GjExcVBkiT89NNP1W4XQmDmzJmIjY2Fr68vhgwZgsOHD1fb5/z585g4cSKCgoIQEhKC++67D6WlpdX22b17N6699lr4+PggPj4ec+fOrdGW7777Du3bt4ePjw9SUlKwbNkyl/fX4WL9njx5co3nf8SIEdX2udL6/dprr6Fnz54IDAxEVFQUxo4di4yMjGr7ePL32pOvEQ3p+8CBA2s85w8//HC1fa60vr///vvo3Lmz8+RYffr0wfLly523X63P98X6fTU+1xclFODrr78WOp1OfPLJJ2Lfvn3igQceECEhISIvL8/bTWuwF198UXTs2FHk5OQ4P86cOeO8/eGHHxbx8fFi9erVYtu2beKaa64Rffv2dd5usVhEp06dxJAhQ8TOnTvFsmXLREREhJgxY4Zzn2PHjgk/Pz8xffp0sX//fvH2228LtVotVqxY4bF+Llu2TPzzn/8UP/74owAgFi9eXO32f/3rXyI4OFj89NNPYteuXeKmm24SSUlJoqKiwrnPiBEjRGpqqti0aZNYu3ataN26tbjjjjuctxcVFYno6GgxceJEsXfvXrFo0SLh6+srPvjgA+c+69evF2q1WsydO1fs379fPP/880Kr1Yo9e/Z4pd+TJk0SI0aMqPb8nz9/vto+V1q/hw8fLj799FOxd+9ekZ6eLm644QaRkJAgSktLnft46vfa068RDen7ddddJx544IFqz3lRUdEV3feff/5ZLF26VBw6dEhkZGSI5557Tmi1WrF3714hxNX7fF+s31fjc30xiggfvXr1ElOmTHF+b7VaRVxcnHjttde82KrGefHFF0VqamqttxUWFgqtViu+++4757YDBw4IAGLjxo1CCNubm0qlErm5uc593n//fREUFCSMRqMQQoinn35adOzYsdqxx48fL4YP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//79hV6vF82aNRP/+te/arTl22+/FW3bthU6nU507NhRLF261Cv9Li8vF8OGDRORkZFCq9WKxMRE8cADD9R4wbjS+l1bfwFU+53z5O+1J18jLtb3rKwsMWDAABEWFib0er1o3bq1eOqpp6qd+0GIK6/v9957r0hMTBQ6nU5ERkaKwYMHO4OHEFfv811fv6/W5/piJCGE8FydhYiIiJTuqp/zQURERE0LwwcRERF5FMMHEREReRTDBxEREXkUwwcRERF5FMMHEREReRTDBxEREXkUwwcRERF5FMMHEREReRTDBxG53OTJkzF27FhvN4OImiiGDyIiIvIohg8iumTff/89UlJS4Ovri/DwcAwZMgRPPfUUPv/8cyxZsgSSJEGSJKSlpQEAsrOzcdtttyEkJARhYWEYM2YMjh8/7jyeo2Iye/ZsREZGIigoCA8//DBMJpN3OkhEbqHxdgOI6MqUk5ODO+64A3PnzsXNN9+MkpISrF27FnfffTeysrJQXFyMTz/9FAAQFhYGs9mM4cOHo0+fPli7di00Gg1efvlljBgxArt374ZOpwMArF69Gj4+PkhLS8Px48dxzz33IDw8HK+88oo3u0tELsTwQUSXJCcnBxaLBePGjUNiYiIAICUlBQDg6+sLo9GImJgY5/7/+9//IMsyPv74Y0iSBAD49NNPERISgrS0NAwbNgwAoNPp8Mknn8DPzw8dO3bESy+9hKeeegpz5syBSsViLdHVgH/JRHRJUlNTMXjwYKSkpODWW2/FRx99hIKCgjr337VrF44cOYLAwEAEBAQgICAAYWFhMBgMOHr0aLXj+vn5Ob/v06cPSktLkZ2d7db+EJHnsPJBRJdErVZj1apV2LBhA1auXIm3334b//znP7F58+Za9y8tLUX37t3x1Vdf1bgtMjLS3c0loiaE4YOILpkkSejXrx/69euHmTNnIjExEYsXL4ZOp4PVaq22b7du3fDNN98gKioKQUFBdR5z165dqKiogK+vLwBg06ZNCAgIQHx8vFv7QkSew2EXIrokmzdvxquvvopt27YhKysLP/74I86cOYPk5GS0aNECu3fvRkZGBs6ePQuz2YyJEyciIiICY8aMwdq1a5GZmYm0tDT8/e9/x8mTJ53HNZlMuO+++7B//34sW7YML774IqZOncr5HkRXEVY+iOiSBAUFYc2aNZg/fz6Ki4uRmJiIN998EyNHjkSPHj2QlpaGHj16oLS0FH/++ScGDhyINWvW4JlnnsG4ceNQUlKCZs2aYfDgwdUqIYMHD0abNm0wYMAAGI1G3HHHHZg1a5b3OkpELicJIYS3G0FEBNjO81FYWIiffvrJ200hIjdiHZOIiIg8iuGDiIiIPIrDLkRERORRrHwQERGRRzF8EBERkUcxfBAREZFHMXwQERGRRzF8EBERkUcxfBAREZFHMXwQERGRRzF8EBERkUf9P/P/71ZZ2MHbAAAAAElFTkSuQmCC"
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "#画线要注意的是损失是不一定在零到1之间的\n",
    "def plot_learning_curves(record_dict, sample_step=500):\n",
    "    # build DataFrame\n",
    "    train_df = pd.DataFrame(record_dict[\"train\"]).set_index(\"step\").iloc[::sample_step]\n",
    "    val_df = pd.DataFrame(record_dict[\"val\"]).set_index(\"step\")\n",
    "\n",
    "    # plot\n",
    "    for idx, item in enumerate(train_df.columns):\n",
    "        plt.plot(train_df.index, train_df[item], label=f\"train_{item}\")\n",
    "        plt.plot(val_df.index, val_df[item], label=f\"val_{item}\")\n",
    "        plt.grid()\n",
    "        plt.legend()\n",
    "        # plt.xticks(range(0, train_df.index[-1], 10*sample_step), range(0, train_df.index[-1], 10*sample_step))\n",
    "        plt.xlabel(\"step\")\n",
    "\n",
    "        plt.show()\n",
    "\n",
    "plot_learning_curves(record)  #横坐标是 steps"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 测试集"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 29,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2024-07-19T07:22:25.784511Z",
     "start_time": "2024-07-19T07:22:25.706962500Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "loss:     0.3432\n"
     ]
    }
   ],
   "source": [
    "model.eval()\n",
    "loss = evaluating(model, val_loader, loss_fct)\n",
    "print(f\"loss:     {loss:.4f}\")"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.9.7"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
